using System.Collections.Generic;
using System;
using System.ComponentModel;
using System.Linq;
using LightPersist.Exceptions;
using LightPersist.Metainfo;

namespace LightPersist
{
	public abstract class ModelBase : INotifyPropertyChanged
	{
		// predefined lists for Strong and Weak back references
		// Strong references determine the Life cycle of the object
		internal const string IsRootName = "__IsRoot";
		internal const string StrongBackReferenceListName = "__StrongBackRefs";
		internal const string WeakBackReferenceListName = "__WeakBackRefs";
		internal static readonly ClassMetainfo Metainfo = new ClassMetainfo { Name = DomainMetainfo.ModelBaseAqn, TypeClrPersist = typeof(ModelBase), };
		readonly Dictionary<string, LightListCore> _cacheOfLists = new Dictionary<string, LightListCore>();

		public readonly Type RealType;
		public readonly string RealTypeName;
		bool _isReadonly = true;

		protected ModelBase(LightPersistContext ctx)
		{
			RealType = GetType();
			RealTypeName = RealType.AssemblyQualifiedName;
			ctx.InitializeModel(this);
		}

		public bool IsReadonly
		{
			get { return _isReadonly; }
			internal set
			{
				if (_isReadonly != value)
				{
					_isReadonly = value;
					OnPropertyChanged("IsReadonly");
				}
			}
		}

		public IEnumerable<ModelBase> BackReferences
		{
			get { return StrongBackReferences.Union(WeakBackReferences).Select(x => ObjectState.LightPersistContext.GetObjectByID(x.ID)); }
		}

		internal bool IsRoot
		{
			get { return GetValue<bool>(IsRootName); }
		}

		internal bool IsAlive
		{
			get { return StrongBackReferences.Any() || IsRoot; }
		}

		internal List<BackReference> StrongBackReferences
		{
			get { return ObjectState.Get(StrongBackReferenceListName) as List<BackReference>; }
		}

		internal List<BackReference> WeakBackReferences
		{
			get { return ObjectState.Get(WeakBackReferenceListName) as List<BackReference>; }
		}

		internal ObjectState ObjectState { set; get; }

		public Guid ID
		{
			get { return ObjectState.Identity; }
		}

		#region INotifyPropertyChanged Members

		public event PropertyChangedEventHandler PropertyChanged;

		#endregion

		public override bool Equals(object obj)
		{
			var another = obj as ModelBase;
			if (another == null)
			{
				return false;
			}

			return ObjectState.Identity == another.ObjectState.Identity;
		}

		public override int GetHashCode()
		{
			return ObjectState.Identity.GetHashCode();
		}

		protected internal virtual T GetValue<T>(string propName)
		{
			var property = ObjectState.ClassMetainfo.GetPropertyInfo(propName);
			if (property != null && property.IsModelProperty)
			{
				var referenceValue = GetReferenceValue(propName);
				var valueWrapper = referenceValue as ValueModelWrapper;
				if (valueWrapper != null)
				{
					return (T)valueWrapper.Value;
				}
				return (T)(object)referenceValue;
			}
			return (T)(ObjectState.LightPersistContext.GetValue(this, propName) ?? default(T));
		}

		protected internal ModelBase GetReferenceValue(string propName)
		{
			var singleElementList = GetList(propName);
			return singleElementList.Count == 0 ? default(ModelBase) : singleElementList[0];
		}

		protected internal void SetReferenceValue(ModelBase value, string propName)
		{
			var singleElementList = GetList(propName);

			if (singleElementList.Count > 1)
			{
				throw new LightPersistException("Inconsistent data. List must not contain more than 1 element");
			}

			// simualte assignment through list
			if (singleElementList.Count == 0 && value != null)
			{
				singleElementList.Add(value);
			}
			else if (singleElementList.Count == 1)
			{
				// we should replace object
				singleElementList.RemoveAt(0);

				if (value != null)
				{
					singleElementList.Add(value);
				}
			}
		}

		protected internal void SetValue<T>(object value, string propName)
		{
			var property = ObjectState.ClassMetainfo.GetPropertyInfo(propName);
			if (property != null && property.IsModelProperty)
			{
				if (property.IsUntyped && !(value is ModelBase))
				{
					value = ObjectState.LightPersistContext.CreateValueWrapper(value);
				}
				SetReferenceValue((ModelBase)value, propName);
			}
			else
			{
				if (value is ModelBase)
				{
					throw new LightPersistException("Property '{0}' is primitive. You can not assign model entities. If you use untyped proeprty (of 'object' type, or interface) you can define '{1}' to {2}".Arg(propName, PropertyMetainfo.UntypedBehaviourName, UntypedPropertyBehaviour.AllowModel));
				}
				ObjectState.LightPersistContext.SetValue(this, propName, value);
			}
		}

		protected internal ILightListProxy<T> GetList<T>(string propName) // where T : ModelBase
		{
			var resultList = GetList(propName);
			return LightListProxy<T>.Factory(resultList);
		}

		internal LightListCore GetList(string propName)
		{
			// get list from cache or create new list
			LightListCore resultList;
			if (!_cacheOfLists.TryGetValue(propName, out resultList))
			{
				resultList = ObjectState.LightPersistContext.GetList(this, propName);
				_cacheOfLists[propName] = resultList;
			}

			return resultList;
		}

		internal void Initialize()
		{
			if (StrongBackReferences == null)
			{
				ObjectState.Set(StrongBackReferenceListName, new List<BackReference>());
			}

			if (WeakBackReferences == null)
			{
				ObjectState.Set(WeakBackReferenceListName, new List<BackReference>());
			}

			_isReadonly = true;
		}

		internal void RemoveAllChildrens()
		{
			// TODO Create more simply and safe way to recive all lists, including one-element-lists
			foreach (var prop in ObjectState.ClassMetainfo.PropertiesAll)
			{
				if (prop.IsList || ObjectState.ClassMetainfo.GetPropertyInfo(prop.Type) != null)
				{
					var list = ObjectState.LightPersistContext.GetList(this, prop.Name);
					list.Clear();
				}
			}
		}

		/// <summary>
		/// FakeRoot#2
		/// </summary>
		/// <returns></returns>
		public override string ToString()
		{
			return GetType().Name + ID.ToStringDebug();
		}

		internal void OnPropertyChanged(string propertyName)
		{
			if (PropertyChanged != null)
			{
				PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
			}
		}
	}
}